Upgrade to Pro — share decks privately, control downloads, hide ads and more …

MySQLのALTER TABLEについて少々

MySQLのALTER TABLEについて少々

2020/05/11 GMO Technology Bootcamp 2020

yoku0825

May 11, 2020
Tweet

More Decks by yoku0825

Other Decks in Technology

Transcript

  1. いいわけ 最初は「MySQLと正規化」みたいな話をしようと思っていた でもそういう話、昔やったから またやるのめんどくさい どうせなら世に出てない情報の方が 面白い 有益かなって思った ‐ MySQLと正規形のはなし ‐

    設計段階から正規化できてればいいけど、そうでない場合は運用中に直さないとい けない あるいは完全に正規化されていようと、仕様が変わればスキーマは変わるもの ‐ となると必要なものは何だ? ALTER TABLEだ! ってなった 多少反省はしている 9/77
  2. 基本構文 ALTER TABLE tbl_name [alter_option [, alter_option] ...] [partition_options] alter_option

    は特定の組み合わせを除いて列挙可能 インデックスとかまとめて複数張った方がI/Oのインプット側的に有利 追加したカラムにそのままインデックス張るとか、新しいインデックス張るついでに古いイン デックスを消すとか ‐ 15/77
  3. table_option(抜粋) table_option: { AUTO_INCREMENT [=] value | [DEFAULT] CHARACTER SET

    [=] charset_name | [DEFAULT] COLLATE [=] collation_name | COMMENT [=] 'string' | COMPRESSION [=] {'ZLIB' | 'LZ4' | 'NONE'} | {DATA | INDEX} DIRECTORY [=] 'absolute path to directory' | ENCRYPTION [=] {'Y' | 'N'} | ENGINE [=] engine_name | ROW_FORMAT [=] {DEFAULT | DYNAMIC | FIXED | COMPRESSED | REDUNDANT | COMPACT} | STATS_AUTO_RECALC [=] {DEFAULT | 0 | 1} | STATS_PERSISTENT [=] {DEFAULT | 0 | 1} | STATS_SAMPLE_PAGES [=] value | TABLESPACE tablespace_name [STORAGE {DISK | MEMORY}] } 16/77
  4. alter_option(1) alter_option: { | ADD [COLUMN] col_name column_definition [FIRST |

    AFTER col_name] | ADD [COLUMN] (col_name column_definition,...) | CHANGE [COLUMN] old_col_name new_col_name column_definition [FIRST | AFTER col_name] | ALTER [COLUMN] col_name {SET DEFAULT {literal | (expr)} | DROP DEFAULT} | DROP [COLUMN] col_name | MODIFY [COLUMN] col_name column_definition [FIRST | AFTER col_name] | RENAME COLUMN old_col_name TO new_col_name 17/77
  5. たとえば こんなテーブルがあるじゃろ? num は PRIMARY KEY ‐ mysql80 66> SELECT

    * FROM t1; +------+-------+ | num | val | +------+-------+ | 1 | one | | 2 | two | | 3 | three | +------+-------+ 3 rows in set (0.00 sec) 20/77
  6. その時 ibd ファイルの中身はこんなんなってるじゃろ? 0200200 \0 \0 001 \0 \0 \0

    * Z 232 201 \0 \0 001 \v 001 020 0200220 o n e 003 \0 \0 \0 030 \0 033 200 \0 \0 002 \0 \0 <-- one 0200240 \0 * Z 232 201 \0 \0 001 \v 001 035 t w o 005 \0 <-- two 0200260 \0 \0 377 273 200 \0 \0 003 \0 \0 \0 * Z 232 201 0200300 \0 \0 001 \v 001 * t h r e e \0 \0 \0 \0 \0 <-- three 21/77
  7. カラムを足すじゃろ? mysql80 68> ALTER TABLE t1 ADD COLUMN val_j varchar(32)

    DEFAULT 'hoge', ALGORITHM= INPLAC E; Query OK, 0 rows affected (0.02 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql80 68> SELECT * FROM t1; +------+-------+-------+ | num | val | val_j | +------+-------+-------+ | 1 | one | hoge | | 2 | two | hoge | | 3 | three | hoge | +------+-------+-------+ 3 rows in set (0.00 sec) 22/77
  8. こうなるんじゃよ 0240200 200 \0 \0 001 \0 \0 \0 *

    Z 232 201 \0 \0 001 \v 001 0240220 020 o n e h o g e 004 003 \0 \0 \0 030 \0 <-- onehoge 0240240 200 \0 \0 002 \0 \0 \0 * Z 232 201 \0 \0 001 \v 001 0240260 035 t w o h o g e 004 005 \0 \0 \0 377 260 <-- twohoge 0240300 200 \0 \0 003 \0 \0 \0 * Z 232 201 \0 \0 001 \v 001 0240320 * t h r e e h o g e \0 \0 \0 \0 \0 \0 <-- threehoge 23/77
  9. INSTANT ADD COLUMN カラムを足す前のt1と同じ構造のt2にALTER TABLEするじゃろ mysql80 69> ALTER TABLE t2

    ADD COLUMN val_j varchar(32) DEFAULT 'hoge', ALGORITHM= INSTAN T; Query OK, 0 rows affected (0.02 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql80 70> SELECT * FROM t2; +------+-------+-------+ | num | val | val_j | +------+-------+-------+ | 1 | one | hoge | | 2 | two | hoge | | 3 | three | hoge | +------+-------+-------+ 3 rows in set (0.00 sec) 29/77
  10. INSTANT ADD COLUMN ないんじゃよ 0200200 \0 \0 001 \0 \0

    \0 * Z 301 202 \0 \0 \0 216 001 020 0200220 o n e 003 \0 \0 \0 030 \0 033 200 \0 \0 002 \0 \0 <-- one 0200240 \0 * Z 301 202 \0 \0 \0 216 001 035 t w o 005 \0 <-- two 0200260 \0 \0 377 273 200 \0 \0 003 \0 \0 \0 * Z 301 202 0200300 \0 \0 \0 216 001 * t h r e e \0 \0 \0 \0 \0 <-- three 30/77
  11. INSTANT ADD COLUMN INSERTした時に初めて mysql80 76> INSERT INTO t2 VALUES

    (4, 'four', 'yon'); Query OK, 1 row affected (0.01 sec) mysql80 76> SELECT * FROM t2; +-----+-------+-------+ | num | val | val_j | +-----+-------+-------+ | 1 | one | hoge | | 2 | two | hoge | | 3 | three | hoge | | 4 | four | yon | +-----+-------+-------+ 4 rows in set (0.00 sec) 31/77
  12. INSTANT ADD COLUMN 出てくる 0200200 \0 \0 001 \0 \0

    \0 * Z 353 202 \0 \0 \0 235 001 020 0200220 o n e 003 \0 \0 \0 030 \0 033 200 \0 \0 002 \0 \0 <-- one 0200240 \0 * Z 353 202 \0 \0 \0 235 001 035 t w o 005 \0 <-- two 0200260 \0 \0 \0 037 200 \0 \0 003 \0 \0 \0 * Z 353 202 0200300 \0 \0 \0 235 001 * t h r e e 003 004 \0 005 200 <-- three 0200320 \0 ( 377 234 200 \0 \0 004 \0 \0 \0 * [ \0 201 \0 0200340 \0 \0 242 001 020 f o u r y o n \0 \0 \0 \0 <-- four, yon 32/77
  13. alter_option(2) alter_option: { | ADD [CONSTRAINT [symbol]] PRIMARY KEY [index_type]

    (key_part,...) [index_option] ... | ADD [CONSTRAINT [symbol]] UNIQUE [INDEX | KEY] [index_name] [index_type] (key_part,...) [index_option] ... | ADD [CONSTRAINT [symbol]] FOREIGN KEY [index_name] (col_name,...) reference_definition | ADD {INDEX | KEY} [index_name] [index_type] (key_part,...) [index_option] ... | ALTER INDEX index_name {VISIBLE | INVISIBLE} | DROP {INDEX | KEY} index_name | DROP PRIMARY KEY | DROP FOREIGN KEY fk_symbol | RENAME {INDEX | KEY} old_index_name TO new_index_name 39/77
  14. インデックス操作系 fast index creationから更に発展して、「ALTER TABLEが始まった時点のデータ (これはMVCCで保証できる)をfast index creationして」「ALTER TABLEが始まっ てから終わるまでの間の更新差分だけ新しいインデックスに反映させればいいん

    じゃ?」 後者を記録するための領域を「(ALTER)オンラインログ」と呼ぶ ‐ ALTER TABLEが「始まった時点のデータを保証する」のと「最後の差分のマージ」はロックで 保護する ‐ InnoDBは行のデータもインデックスの一種なので「カラム操作系」もこのテク ニックを使っている 「別の領域にクラスターインデックスを新しく作ってそれをすり替え」ている ‐ 42/77
  15. alter_option(2.5) alter_option: { | ADD {FULLTEXT | SPATIAL} [INDEX |

    KEY] [index_name] (key_part,...) [index_option] ... | ADD [CONSTRAINT [symbol]] CHECK (expr) [[NOT] ENFORCED] | DROP {CHECK | CONSTRAINT} symbol | ALTER {CHECK | CONSTRAINT} symbol [NOT] ENFORCED 45/77
  16. alter_option(3) alter_option: { | ALGORITHM [=] {DEFAULT | INSTANT |

    INPLACE | COPY} | {DISCARD | IMPORT} TABLESPACE | FORCE | LOCK [=] {DEFAULT | NONE | SHARED | EXCLUSIVE} | ORDER BY col_name [, col_name] ... | RENAME [TO | AS] new_tbl_name | {WITHOUT | WITH} VALIDATION } 47/77
  17. alter_option(3) alter_option: { | ALGORITHM [=] {DEFAULT | INSTANT |

    INPLACE | COPY} | {DISCARD | IMPORT} TABLESPACE | FORCE | LOCK [=] {DEFAULT | NONE | SHARED | EXCLUSIVE} | ORDER BY col_name [, col_name] ... | RENAME [TO | AS] new_tbl_name | {WITHOUT | WITH} VALIDATION } 49/77
  18. alter_option(3) alter_option: { | ALGORITHM [=] {DEFAULT | INSTANT |

    INPLACE | COPY} | {DISCARD | IMPORT} TABLESPACE | FORCE | LOCK [=] {DEFAULT | NONE | SHARED | EXCLUSIVE} | ORDER BY col_name [, col_name] ... | RENAME [TO | AS] new_tbl_name | {WITHOUT | WITH} VALIDATION } 51/77
  19. 制御系のオプション ORDER BY を指定すると、データ本体の並び順を ORDER BY オプションで指定した通 りに並べ替えてくれる 「は?」って思ったでしょ。俺も8年位前にそう思った。 ‐

    ただしプライマリーキー(厳密にはクラスターインデックス)がない場合に限る というかこういうのを真似してはいけない ‐ 52/77
  20. ORDER BY PRIMARY KEYなしで作る mysql80 8> CREATE TABLE t3 (num

    int, val varchar(32)); Query OK, 0 rows affected (0.02 sec) mysql80 8> INSERT INTO t3 VALUES (1, 'one'), (2, 'two'), (3, 'three'); Query OK, 3 rows affected (0.00 sec) Records: 3 Duplicates: 0 Warnings: 0 mysql80 8> SELECT * FROM t3; -- ORDER BYがないSELECTは行が見つかった順に返す +------+-------+ | num | val | +------+-------+ | 1 | one | | 2 | two | | 3 | three | +------+-------+ 3 rows in set (0.00 sec) 53/77
  21. ORDER BY とすると、内部的に6バイトの「暗黙の行ID」が作られてその通りに並ぶ 0200220 001 020 200 \0 \0 001

    o n e 003 \0 \0 \0 030 \0 ! <-- one 0200240 \0 \0 \0 002 302 001 \0 \0 \0 J h N 201 \0 \0 \0 0200260 215 001 037 200 \0 \0 002 t w o 005 \0 \0 \0 377 <-- two 0200300 257 \0 \0 \0 002 302 002 \0 \0 \0 J h N 201 \0 \0 0200320 \0 215 001 . 200 \0 \0 003 t h r e e \0 \0 \0 <-- three 54/77
  22. ORDER BY ORDER BY val DESC にしてみる mysql80 8> ALTER

    TABLE t3 ORDER BY val DESC; Query OK, 3 rows affected (0.04 sec) Records: 3 Duplicates: 0 Warnings: 0 mysql80 8> SELECT * FROM t3; -- ORDER BYがないSELECTは行が見つかった順に返す +------+-------+ | num | val | +------+-------+ | 2 | two | | 3 | three | | 1 | one | +------+-------+ 3 rows in set (0.00 sec) 55/77
  23. ORDER BY ibdファイルの中身はこうなってる 0200220 \0 \0 200 \0 \0 002

    t w o 005 \0 \0 \0 030 \0 # <-- two 0200240 \0 \0 \0 002 302 004 \0 \0 \0 J h Y 200 \0 \0 \0 0200260 \0 \0 \0 200 \0 \0 003 t h r e e 003 \0 \0 \0 <-- three 0200300 377 255 \0 \0 \0 002 302 005 \0 \0 \0 J h Y 200 0200320 \0 \0 \0 \0 \0 \0 200 \0 \0 001 o n e \0 \0 \0 <-- one 56/77
  24. ORDER BY 古の時代、「1日1回くらい ALTER TABLE .. ORDER BY updated_at DESC

    ってやれ ばSELECTに ORDER BY 書かなくても(=ソートのためのコストを払わなくても)それ なりに最新のものが返ってくるんじゃ?」と思って作られたんではなかろうか 当然ながらこんなものを使ってはいけない。 だがこのへんがMySQLっぽくて楽し い MyISAMでできるのは知ってたけどまさかInnoDBにも対応しているとは思わなかった… ‐ 57/77
  25. 制御系のオプション ALGORITHM [=] {DEFAULT | INSTANT | INPLACE | COPY}

    テーブルコピーまたはfast index creation型(=インプレース)またはINSTANT ADD COLUMN ‐ LOCK [=] {DEFAULT | NONE | SHARED | EXCLUSIVE} 読み書きを許可する(= NONE)、読み取りだけ許可する(= SHARED) ‐ 59/77
  26. 制御系のオプション おまじない代わりに ALGORITHM = INPLACE, LOCK = NONE を付けておくと「オンラ インでできないまたはテーブルコピーが必要なALTER

    TABLEはエラーになる」 mysql80 8> ALTER TABLE t1 ADD COLUMN c1 int, ALGORITHM= INPLACE, LOCK= NONE; Query OK, 0 rows affected (0.03 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql80 8> ALTER TABLE t1 MODIFY c1 bigint, ALGORITHM= INPLACE, LOCK= NONE; ERROR 1846 (0A000): ALGORITHM=INPLACE is not supported. Reason: Cannot change column type INPLACE. Try ALGORITHM=COPY. mysql80 8> ALTER TABLE t1 ADD FULLTEXT KEY(val), ALGORITHM= INPLACE, LOCK= NONE; ERROR 1846 (0A000): LOCK=NONE is not supported. Reason: Fulltext index creation requires a lock. Try LOCK=SHARED. 60/77
  27. ALTER TABLEのツボ ALTER TABLEが「始まった時点のデータを保証する」のと「最後の差分の マージ」はロックで保護する この2か所が詰まりがち ALTER TABLE開始時のロックは「テーブルメタデータロック」 本来時間は短いが「ロックの粒度」はかなりゴツい ‐

    シンプルな「接続してクエリー投げて切断」みたいなやつなら大体大丈夫なんだけど、トラン ザクションかけて他所のAPI叩いて戻りを待って…みたいな作りになっていると相性が悪い というか他所のAPIを叩くケース、失敗したように見えて応答が返ってきてないだけの可能性がたくさんあるのでシ ンプルなトランザクションじゃなくて「二度書き」のトランザクションにすると良い ‐ あとは集計バッチとかかな… ‐ 61/77
  28. テーブルメタデータロック mysql80 9> BEGIN; Query OK, 0 rows affected (0.00

    sec) mysql80 9> SELECT * FROM t1; -- 単なるSELECTは行ロックは取らないが Empty set (0.00 sec) mysql80 10> ALTER TABLE t1 Engine = InnoDB; -- 9のトランザクションに阻まれてロック待ち mysql80 11> SELECT * FROM t1; -- 10のALTER TABLEに阻まれてロック待ち 62/77
  29. テーブルメタデータロック mysql80 9> SHOW PROCESSLIST; +----+-----------------+-----------+------+---------+-------+---------------------------- -----+--------------------------------+ | Id |

    User | Host | db | Command | Time | State | Info | +----+-----------------+-----------+------+---------+-------+---------------------------- -----+--------------------------------+ | 5 | event_scheduler | localhost | NULL | Daemon | 34966 | Waiting on empty queue | NULL | | 9 | root | localhost | d1 | Query | 0 | starting | SHOW PROCESSLIST | | 10 | root | localhost | d1 | Query | 16 | Waiting for table metadata lock | ALTER TABLE t1 Engine = InnoDB | | 11 | root | localhost | d1 | Query | 4 | Waiting for table metadata lock | SELECT * FROM t1 | +----+-----------------+-----------+------+---------+-------+---------------------------- -----+--------------------------------+ 4 rows in set (0.00 sec) 63/77
  30. ALTER TABLEのツボ 少なくともALTER TABLE開始時と終了時は innotop とかで監視を innotop/innotop: A realtime terminal-based

    top-like monitor for MySQL ‐ 万一ブロッキングALTER TABLEになっても早期発見が可能なので ‐ 開始時に詰まったら止められるけど、終了時に詰まっても「止めてはいけない」 (し、だいたいCtrl+Cくらいじゃ「止まらない」) 最後の差分マージはロックで詰まるわけではなくて単に負荷が異様に高くなるだけだけ (InnoDBログにぐわっと書き込むから) ‐ 64/77
  31. Rolling Schema Upgrade カラムの増減でこれをやると往々にして死ぬ これはアプリケーションが吐くSQLがカラムの増減に強く && binlog_format = STATEMENT でなくてはならないから

    ‐ 後者を設定すると大体別のところで死ぬので、binlog_format= ROWが推奨される世の中 ‐ 上手く使いきれる人はニンジャ ‐ だからこそINSTANT ADD COLUMNが生まれたとも考えられる 75/77